/*
 * Copyright (c) 2016 Kevin Herron
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * and Eclipse Distribution License v1.0 which accompany this distribution.
 *
 * The Eclipse Public License is available at
 *   http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at
 *   http://www.eclipse.org/org/documents/edl-v10.html.
 */

package com.dayu.unitalk.ua.server;

import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ubyte;
import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint;
import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ulong;
import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ushort;

import java.lang.reflect.Array;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;

import org.eclipse.milo.opcua.sdk.core.AccessLevel;
import org.eclipse.milo.opcua.sdk.core.Reference;
import org.eclipse.milo.opcua.sdk.core.ValueRank;
import org.eclipse.milo.opcua.sdk.server.OpcUaServer;
import org.eclipse.milo.opcua.sdk.server.api.AccessContext;
import org.eclipse.milo.opcua.sdk.server.api.DataItem;
import org.eclipse.milo.opcua.sdk.server.api.MethodInvocationHandler;
import org.eclipse.milo.opcua.sdk.server.api.MonitoredItem;
import org.eclipse.milo.opcua.sdk.server.api.Namespace;
import org.eclipse.milo.opcua.sdk.server.api.nodes.VariableNode;
import org.eclipse.milo.opcua.sdk.server.nodes.AttributeContext;
import org.eclipse.milo.opcua.sdk.server.nodes.ServerNode;
import org.eclipse.milo.opcua.sdk.server.nodes.UaFolderNode;
import org.eclipse.milo.opcua.sdk.server.nodes.UaMethodNode;
import org.eclipse.milo.opcua.sdk.server.nodes.UaNode;
import org.eclipse.milo.opcua.sdk.server.nodes.UaVariableNode;
import org.eclipse.milo.opcua.sdk.server.nodes.delegates.AttributeDelegate;
import org.eclipse.milo.opcua.sdk.server.nodes.delegates.AttributeDelegateChain;
import org.eclipse.milo.opcua.sdk.server.util.SubscriptionModel;
import org.eclipse.milo.opcua.stack.core.AttributeId;
import org.eclipse.milo.opcua.stack.core.Identifiers;
import org.eclipse.milo.opcua.stack.core.StatusCodes;
import org.eclipse.milo.opcua.stack.core.UaException;
import org.eclipse.milo.opcua.stack.core.types.builtin.ByteString;
import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
import org.eclipse.milo.opcua.stack.core.types.builtin.DateTime;
import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText;
import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName;
import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode;
import org.eclipse.milo.opcua.stack.core.types.builtin.Variant;
import org.eclipse.milo.opcua.stack.core.types.builtin.XmlElement;
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger;
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UShort;
import org.eclipse.milo.opcua.stack.core.types.enumerated.NodeClass;
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn;
import org.eclipse.milo.opcua.stack.core.types.structured.ReadValueId;
import org.eclipse.milo.opcua.stack.core.types.structured.WriteValue;
import org.eclipse.milo.opcua.stack.core.util.FutureUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.Lists;

public class UnitalkNamespace implements Namespace {

	private static final String ROOT_NODE = "devices";

	public static final String NAMESPACE_URI = "urn:hddznet:unitalk:device";

	private final static Logger LOGGER = LoggerFactory.getLogger(UnitalkNamespace.class);

	@Deprecated
	private static final Object[][] STATIC_SCALAR_NODES = new Object[][] {
			{ "Boolean", Identifiers.Boolean, new Variant(false) },
			{ "Byte", Identifiers.Byte, new Variant(ubyte(0x00)) },
			{ "SByte", Identifiers.SByte, new Variant((byte) 0x00) },
			{ "Int16", Identifiers.Int16, new Variant((short) 16) }, { "Int32", Identifiers.Int32, new Variant(32) },
			{ "Int64", Identifiers.Int64, new Variant(64L) }, { "UInt16", Identifiers.UInt16, new Variant(ushort(16)) },
			{ "UInt32", Identifiers.UInt32, new Variant(uint(32)) },
			{ "UInt64", Identifiers.UInt64, new Variant(ulong(64L)) },
			{ "Float", Identifiers.Float, new Variant(3.14f) }, { "Double", Identifiers.Double, new Variant(3.14d) },
			{ "String", Identifiers.String, new Variant("string value") },
			{ "DateTime", Identifiers.DateTime, new Variant(DateTime.now()) },
			{ "Guid", Identifiers.Guid, new Variant(UUID.randomUUID()) },
			{ "ByteString", Identifiers.ByteString,
					new Variant(new ByteString(new byte[] { 0x01, 0x02, 0x03, 0x04 })) },
			{ "XmlElement", Identifiers.XmlElement, new Variant(new XmlElement("<a>hello</a>")) },
			{ "LocalizedText", Identifiers.LocalizedText, new Variant(LocalizedText.english("localized text")) },
			{ "QualifiedName", Identifiers.QualifiedName, new Variant(new QualifiedName(1234, "defg")) },
			{ "NodeId", Identifiers.NodeId, new Variant(new NodeId(1234, "abcd")) },

			{ "Duration", Identifiers.Duration, new Variant(1.0) },
			{ "UtcTime", Identifiers.UtcTime, new Variant(DateTime.now()) }, };

	@Deprecated
	private static final Object[][] STATIC_ARRAY_NODES = new Object[][] {
			{ "BooleanArray", Identifiers.Boolean, false }, { "ByteArray", Identifiers.Byte, ubyte(0) },
			{ "SByteArray", Identifiers.SByte, (byte) 0x00 }, { "Int16Array", Identifiers.Int16, (short) 16 },
			{ "Int32Array", Identifiers.Int32, 32 }, { "Int64Array", Identifiers.Int64, 64L },
			{ "UInt16Array", Identifiers.UInt16, ushort(16) }, { "UInt32Array", Identifiers.UInt32, uint(32) },
			{ "UInt64Array", Identifiers.UInt64, ulong(64L) }, { "FloatArray", Identifiers.Float, 3.14f },
			{ "DoubleArray", Identifiers.Double, 3.14d }, { "StringArray", Identifiers.String, "string value" },
			{ "DateTimeArray", Identifiers.DateTime, new Variant(DateTime.now()) },
			{ "GuidArray", Identifiers.Guid, new Variant(UUID.randomUUID()) },
			{ "ByteStringArray", Identifiers.ByteString,
					new Variant(new ByteString(new byte[] { 0x01, 0x02, 0x03, 0x04 })) },
			{ "XmlElementArray", Identifiers.XmlElement, new Variant(new XmlElement("<a>hello</a>")) },
			{ "LocalizedTextArray", Identifiers.LocalizedText, new Variant(LocalizedText.english("localized text")) },
			{ "QualifiedNameArray", Identifiers.QualifiedName, new Variant(new QualifiedName(1234, "defg")) },
			{ "NodeIdArray", Identifiers.NodeId, new Variant(new NodeId(1234, "abcd")) } };

	private final Random random = new Random();

	private final SubscriptionModel subscription;

	private final OpcUaServer server;
	private final UShort namespace;

	private UaFolderNode root;

	public UnitalkNamespace(OpcUaServer server, UShort namespace) {

		this.server = server;
		this.namespace = namespace;

		subscription = new SubscriptionModel(server, this);

		try {

			// 根目录
			NodeId folderNodeId = new NodeId(namespace, ROOT_NODE);

			root = new UaFolderNode(server.getNodeMap(), folderNodeId, new QualifiedName(namespace, ROOT_NODE),
					LocalizedText.english(ROOT_NODE));

			server.getNodeMap().addNode(root);

			server.getUaNamespace().addReference(Identifiers.ObjectsFolder, Identifiers.Organizes, true,
					folderNodeId.expanded(), NodeClass.Object);

			// // comment for production use
			// // Add the rest of the nodes
			// addVariableNodes(folderNode);
			// addMethodNode(folderNode);

		} catch (UaException e) {
			LOGGER.error("Error adding nodes: {}", e.getMessage(), e);
		}
	}

	public synchronized ServerNode write(String id) throws UaException {

		ServerNode snode = null;
		String[] nodes = id.split("_");
		UaFolderNode current = root;

		for (int i = 0; i < nodes.length; i++) {

			if (i < nodes.length - 1) { // folders

				String tag = nodes[i];
				ServerNode node = server.getNodeMap().get(new NodeId(namespace, tag));

				if (node != null) {

					current = (UaFolderNode) node;
					continue;

				} else {

					UaFolderNode station = new UaFolderNode(server.getNodeMap(), new NodeId(namespace, tag),
							new QualifiedName(namespace, tag), LocalizedText.english(tag));

					server.getNodeMap().addNode(station);
					current.addOrganizes(station);

					LOGGER.info("Create folder {} on {}", tag, current.getNodeId().getIdentifier());
					current = station;
				}

			} else { // node

				String tag = id;
				ServerNode node = server.getNodeMap().get(new NodeId(namespace, tag));

				if (node == null) {

					NodeId typeId = Identifiers.Double;

					Variant variant = new Variant(0);

					node = new UaVariableNode.UaVariableNodeBuilder(server.getNodeMap())
							.setNodeId(new NodeId(namespace, id))
							.setAccessLevel(ubyte(AccessLevel.getMask(AccessLevel.READ_WRITE)))
							.setUserAccessLevel(ubyte(AccessLevel.getMask(AccessLevel.READ_WRITE)))
							.setBrowseName(new QualifiedName(namespace, id)).setDisplayName(LocalizedText.english(id))
							.setDataType(typeId).setTypeDefinition(Identifiers.BaseDataVariableType)
							.setValue(new DataValue(variant, StatusCode.BAD)).build();

					server.getNodeMap().addNode(node);
					current.addOrganizes((UaNode) node);
					LOGGER.info("Create tag {} on {}", id, current.getNodeId().getIdentifier());
				}

				snode = node;
			}
		}

		return snode;
	}

	@Override
	public UShort getNamespaceIndex() {
		return namespace;
	}

	@Override
	public String getNamespaceUri() {
		return NAMESPACE_URI;
	}

	@SuppressWarnings("unused")
	@Deprecated
	private void addVariableNodes(UaFolderNode rootNode) {
		addArrayNodes(rootNode);
		addScalarNodes(rootNode);
		addAdminReadableNodes(rootNode);
		addAdminWritableNodes(rootNode);
		addDynamicNodes(rootNode);
	}

	@Deprecated
	private void addArrayNodes(UaFolderNode rootNode) {
		UaFolderNode arrayTypesFolder = new UaFolderNode(server.getNodeMap(),
				new NodeId(namespace, "HelloWorld/ArrayTypes"), new QualifiedName(namespace, "ArrayTypes"),
				LocalizedText.english("ArrayTypes"));

		server.getNodeMap().addNode(arrayTypesFolder);
		rootNode.addOrganizes(arrayTypesFolder);

		for (Object[] os : STATIC_ARRAY_NODES) {
			String name = (String) os[0];
			NodeId typeId = (NodeId) os[1];
			Object value = os[2];
			Object array = Array.newInstance(value.getClass(), 4);
			for (int i = 0; i < 4; i++) {
				Array.set(array, i, value);
			}
			Variant variant = new Variant(array);

			UaVariableNode node = new UaVariableNode.UaVariableNodeBuilder(server.getNodeMap())
					.setNodeId(new NodeId(namespace, "HelloWorld/ArrayTypes/" + name))
					.setAccessLevel(ubyte(AccessLevel.getMask(AccessLevel.READ_WRITE)))
					.setUserAccessLevel(ubyte(AccessLevel.getMask(AccessLevel.READ_WRITE)))
					.setBrowseName(new QualifiedName(namespace, name)).setDisplayName(LocalizedText.english(name))
					.setDataType(typeId).setTypeDefinition(Identifiers.BaseDataVariableType)
					.setValueRank(ValueRank.OneDimension.getValue()).setArrayDimensions(new UInteger[] { uint(0) })
					.build();

			node.setValue(new DataValue(variant));

			node.setAttributeDelegate(new ValueLoggingDelegate());

			server.getNodeMap().addNode(node);
			arrayTypesFolder.addOrganizes(node);
		}
	}

	@Deprecated
	private void addScalarNodes(UaFolderNode rootNode) {
		UaFolderNode scalarTypesFolder = new UaFolderNode(server.getNodeMap(),
				new NodeId(namespace, "HelloWorld/ScalarTypes"), new QualifiedName(namespace, "ScalarTypes"),
				LocalizedText.english("ScalarTypes"));

		server.getNodeMap().addNode(scalarTypesFolder);
		rootNode.addOrganizes(scalarTypesFolder);

		for (Object[] os : STATIC_SCALAR_NODES) {
			String name = (String) os[0];
			NodeId typeId = (NodeId) os[1];
			Variant variant = (Variant) os[2];

			UaVariableNode node = new UaVariableNode.UaVariableNodeBuilder(server.getNodeMap())
					.setNodeId(new NodeId(namespace, "HelloWorld/ScalarTypes/" + name))
					.setAccessLevel(ubyte(AccessLevel.getMask(AccessLevel.READ_WRITE)))
					.setUserAccessLevel(ubyte(AccessLevel.getMask(AccessLevel.READ_WRITE)))
					.setBrowseName(new QualifiedName(namespace, name)).setDisplayName(LocalizedText.english(name))
					.setDataType(typeId).setTypeDefinition(Identifiers.BaseDataVariableType).build();

			node.setValue(new DataValue(variant));

			node.setAttributeDelegate(new ValueLoggingDelegate());

			server.getNodeMap().addNode(node);
			scalarTypesFolder.addOrganizes(node);
		}
	}

	@Deprecated
	private void addAdminReadableNodes(UaFolderNode rootNode) {
		UaFolderNode adminFolder = new UaFolderNode(server.getNodeMap(),
				new NodeId(namespace, "HelloWorld/OnlyAdminCanRead"), new QualifiedName(namespace, "OnlyAdminCanRead"),
				LocalizedText.english("OnlyAdminCanRead"));

		server.getNodeMap().addNode(adminFolder);
		rootNode.addOrganizes(adminFolder);

		String name = "String";
		UaVariableNode node = new UaVariableNode.UaVariableNodeBuilder(server.getNodeMap())
				.setNodeId(new NodeId(namespace, "HelloWorld/OnlyAdminCanRead/" + name))
				.setAccessLevel(ubyte(AccessLevel.getMask(AccessLevel.READ_WRITE)))
				.setBrowseName(new QualifiedName(namespace, name)).setDisplayName(LocalizedText.english(name))
				.setDataType(Identifiers.String).setTypeDefinition(Identifiers.BaseDataVariableType).build();

		node.setValue(new DataValue(new Variant("shh... don't tell the lusers")));

		node.setAttributeDelegate(new RestrictedAccessDelegate(identity -> {
			if ("admin".equals(identity)) {
				return AccessLevel.READ_WRITE;
			} else {
				return AccessLevel.NONE;
			}
		}));

		server.getNodeMap().addNode(node);
		adminFolder.addOrganizes(node);
	}

	@Deprecated
	private void addAdminWritableNodes(UaFolderNode rootNode) {
		UaFolderNode adminFolder = new UaFolderNode(server.getNodeMap(),
				new NodeId(namespace, "HelloWorld/OnlyAdminCanWrite"),
				new QualifiedName(namespace, "OnlyAdminCanWrite"), LocalizedText.english("OnlyAdminCanWrite"));

		server.getNodeMap().addNode(adminFolder);
		rootNode.addOrganizes(adminFolder);

		String name = "String";
		UaVariableNode node = new UaVariableNode.UaVariableNodeBuilder(server.getNodeMap())
				.setNodeId(new NodeId(namespace, "HelloWorld/OnlyAdminCanWrite/" + name))
				.setAccessLevel(ubyte(AccessLevel.getMask(AccessLevel.READ_WRITE)))
				.setBrowseName(new QualifiedName(namespace, name)).setDisplayName(LocalizedText.english(name))
				.setDataType(Identifiers.String).setTypeDefinition(Identifiers.BaseDataVariableType).build();

		node.setValue(new DataValue(new Variant("admin was here")));

		node.setAttributeDelegate(new RestrictedAccessDelegate(identity -> {
			if ("admin".equals(identity)) {
				return AccessLevel.READ_WRITE;
			} else {
				return AccessLevel.READ_ONLY;
			}
		}));

		server.getNodeMap().addNode(node);
		adminFolder.addOrganizes(node);
	}

	@Deprecated
	private void addDynamicNodes(UaFolderNode rootNode) {
		UaFolderNode dynamicFolder = new UaFolderNode(server.getNodeMap(), new NodeId(namespace, "HelloWorld/Dynamic"),
				new QualifiedName(namespace, "Dynamic"), LocalizedText.english("Dynamic"));

		server.getNodeMap().addNode(dynamicFolder);
		rootNode.addOrganizes(dynamicFolder);

		// Dynamic Boolean
		{
			String name = "Boolean";
			NodeId typeId = Identifiers.Boolean;
			Variant variant = new Variant(false);

			UaVariableNode node = new UaVariableNode.UaVariableNodeBuilder(server.getNodeMap())
					.setNodeId(new NodeId(namespace, "HelloWorld/Dynamic/" + name))
					.setAccessLevel(ubyte(AccessLevel.getMask(AccessLevel.READ_WRITE)))
					.setBrowseName(new QualifiedName(namespace, name)).setDisplayName(LocalizedText.english(name))
					.setDataType(typeId).setTypeDefinition(Identifiers.BaseDataVariableType).build();

			node.setValue(new DataValue(variant));

			AttributeDelegate delegate = AttributeDelegateChain.create(new AttributeDelegate() {
				@Override
				public DataValue getValue(AttributeContext context, VariableNode node) throws UaException {
					return new DataValue(new Variant(random.nextBoolean()));
				}
			}, ValueLoggingDelegate::new);

			node.setAttributeDelegate(delegate);

			server.getNodeMap().addNode(node);
			dynamicFolder.addOrganizes(node);
		}

		// Dynamic Int32
		{
			String name = "Int32";
			NodeId typeId = Identifiers.Int32;
			Variant variant = new Variant(0);

			UaVariableNode node = new UaVariableNode.UaVariableNodeBuilder(server.getNodeMap())
					.setNodeId(new NodeId(namespace, "HelloWorld/Dynamic/" + name))
					.setAccessLevel(ubyte(AccessLevel.getMask(AccessLevel.READ_WRITE)))
					.setBrowseName(new QualifiedName(namespace, name)).setDisplayName(LocalizedText.english(name))
					.setDataType(typeId).setTypeDefinition(Identifiers.BaseDataVariableType).build();

			node.setValue(new DataValue(variant));

			AttributeDelegate delegate = AttributeDelegateChain.create(new AttributeDelegate() {
				@Override
				public DataValue getValue(AttributeContext context, VariableNode node) throws UaException {
					return new DataValue(new Variant(random.nextInt()));
				}
			}, ValueLoggingDelegate::new);

			node.setAttributeDelegate(delegate);

			server.getNodeMap().addNode(node);
			dynamicFolder.addOrganizes(node);
		}

		// Dynamic Double
		{
			String name = "Double";
			NodeId typeId = Identifiers.Double;
			Variant variant = new Variant(0.0);

			UaVariableNode node = new UaVariableNode.UaVariableNodeBuilder(server.getNodeMap())
					.setNodeId(new NodeId(namespace, "HelloWorld/Dynamic/" + name))
					.setAccessLevel(ubyte(AccessLevel.getMask(AccessLevel.READ_WRITE)))
					.setBrowseName(new QualifiedName(namespace, name)).setDisplayName(LocalizedText.english(name))
					.setDataType(typeId).setTypeDefinition(Identifiers.BaseDataVariableType).build();

			node.setValue(new DataValue(variant));

			AttributeDelegate delegate = AttributeDelegateChain.create(new AttributeDelegate() {
				@Override
				public DataValue getValue(AttributeContext context, VariableNode node) throws UaException {
					return new DataValue(new Variant(random.nextDouble()));
				}
			}, ValueLoggingDelegate::new);

			node.setAttributeDelegate(delegate);

			server.getNodeMap().addNode(node);
			dynamicFolder.addOrganizes(node);
		}
	}

	@Override
	public CompletableFuture<List<Reference>> browse(AccessContext context, NodeId nodeId) {
		ServerNode node = server.getNodeMap().get(nodeId);

		if (node != null) {
			return CompletableFuture.completedFuture(node.getReferences());
		} else {
			return FutureUtils.failedFuture(new UaException(StatusCodes.Bad_NodeIdUnknown));
		}
	}

	@Override
	public void read(ReadContext context, Double maxAge, TimestampsToReturn timestamps,
			List<ReadValueId> readValueIds) {

		List<DataValue> results = Lists.newArrayListWithCapacity(readValueIds.size());

		for (ReadValueId readValueId : readValueIds) {

			ServerNode node = server.getNodeMap().get(readValueId.getNodeId());

			if (node != null) {
				DataValue value = node.readAttribute(new AttributeContext(context), readValueId.getAttributeId(),
						timestamps, readValueId.getIndexRange());

				results.add(value);
			} else {
				results.add(new DataValue(StatusCodes.Bad_NodeIdUnknown));
			}
		}

		context.complete(results);
	}

	@Override
	public void write(WriteContext context, List<WriteValue> writeValues) {

		List<StatusCode> results = Lists.newArrayListWithCapacity(writeValues.size());

		for (WriteValue writeValue : writeValues) {

			ServerNode node = server.getNodeMap().get(writeValue.getNodeId());

			if (node == null) {

				String id = writeValue.getNodeId().getIdentifier().toString();

				try {
					node = write(id);
				} catch (UaException e) {
					e.printStackTrace();
				}
			}

			if (node != null) {

				try {

					Object origin = writeValue.getValue().getValue().getValue();
					Variant variant = new Variant(((Number) origin).doubleValue());

					DataValue value = new DataValue(variant, StatusCode.GOOD, writeValue.getValue().getSourceTime(),
							writeValue.getValue().getServerTime());

					node.writeAttribute(new AttributeContext(context), writeValue.getAttributeId(), value,
							writeValue.getIndexRange());

					results.add(StatusCode.GOOD);

					if (LOGGER.isDebugEnabled()) {
						LOGGER.debug("Wrote value {} to {} attribute of {}", value,
								AttributeId.from(writeValue.getAttributeId()).map(Object::toString).orElse("unknown"),
								node.getNodeId());
					}

				} catch (UaException e) {
					LOGGER.error("Unable to write value={}", writeValue.getValue(), e);
					results.add(e.getStatusCode());
				}

			} else {

				results.add(new StatusCode(StatusCodes.Bad_NodeIdUnknown));
			}
		}

		context.complete(results);
	}

	@Override
	public void onDataItemsCreated(List<DataItem> items) {
		subscription.onDataItemsCreated(items);
	}

	@Override
	public void onDataItemsModified(List<DataItem> items) {
		subscription.onDataItemsModified(items);
	}

	@Override
	public void onDataItemsDeleted(List<DataItem> items) {
		subscription.onDataItemsDeleted(items);
	}

	@Override
	public void onMonitoringModeChanged(List<MonitoredItem> items) {
		subscription.onMonitoringModeChanged(items);
	}

	@Override
	public Optional<MethodInvocationHandler> getInvocationHandler(NodeId methodId) {

		Optional<ServerNode> node = server.getNodeMap().getNode(methodId);

		return node.flatMap(n -> {
			if (n instanceof UaMethodNode) {
				return ((UaMethodNode) n).getInvocationHandler();
			} else {
				return Optional.empty();
			}
		});
	}
}
